use std;
use std::vec::Vec;
use sdl2;
use sdl2::pixels::{Color};
use geom::{self, Len, Off, Pos, Dim, AABB};

/// Represents a element to be drawn onto the screen. A [`Fragment`] may be a simple element such as
/// a rectangle or some text, or a compound element comprised of sub-fragments. Fragments do not
/// typically have an absolute position; rather, their position will be determined in relation to
/// fragments around them.
pub struct Frag {
  calculated_offset: Pos,
  calculated_dim: Dim,
  typ: FragType,
  align_h: AlignH,
  align_v: AlignV,
}

enum FragType {
  Empty,
  Fill(Color),
  PackH { frags: Vec<Frag>, right_left: bool, spacing: Off },
  PackV { frags: Vec<Frag>, bottom_top: bool, spacing: Off },
  Overlay { frags: Vec<Frag> },
  Container(Box<FragContainer>),
}

struct FragContainer {
  inner: Frag,
  padding: Off,
  dim: Dim,
}

#[derive(Copy, Clone, Eq, PartialEq)]
pub enum AlignH { Left, Center, Right }
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum AlignV { Top, Middle, Bottom }

impl From<FragType> for Frag {
  fn from(typ: FragType) -> Frag {
    Frag {
      calculated_offset: geom::ORIGIN,
      calculated_dim: geom::EMPTY,
      align_h: AlignH::Left,
      align_v: AlignV::Middle,
      typ,
    }
  }
}

impl From<FragContainer> for Frag {
  fn from(c: FragContainer) -> Frag {
    FragType::Container(Box::new(c)).into()
  }
}

impl FragContainer {
  fn new(inner: Frag, padding: Off) -> Self {
    FragContainer {
      padding,
      inner,
      dim: geom::MAX_DIM,
    }
  }
}

/// Returns a fragment with no size and no appearance.
pub fn empty() -> Frag {
  FragType::Empty.into()
}

/// Returns a fragment which will take up the entire area, filling with the given color.
pub fn fill(color: Color) -> Frag {
  FragType::Fill(color).into()
}

/// Returns a fragment which will render as a rectangle (initial 0x0 pixels), filled with solid
/// color `color`. The size of the rectangle can be configured using the `.dim()`, `.width()`, and
/// `.height()` functions.
pub fn rect(dim: Dim, color: Color) -> Frag {
  FragContainer {
    inner: fill(color),
    padding: 0,
    dim,
  }.into()
}

/// Returns a horizontally-packed fragment composed of the fragments produced by the given
/// iterator. Each fragment (chosen from left to right) will greedily take up as much horizonal
/// space as possible, then be placed next to eachother. Spacing between fragments can be
/// specified using the function `spacing`.
pub fn pack_lr<I: IntoIterator<Item=Frag>>(it: I) -> Frag {
  FragType::PackH { frags: it.into_iter().collect(), right_left: false, spacing: 0 }.into()
}

/// Like `pack_lr`, but fragment size are calculated from right to left.
pub fn pack_rl<I: IntoIterator<Item=Frag>>(it: I) -> Frag {
  FragType::PackH { frags: it.into_iter().collect(), right_left: true, spacing: 0 }.into()
}

/// Returns a vertically-packed fragment composed of the fragments produced by the given
/// iterator. Each fragment (chosen from top to bottom) will greedily take up as much vertical
/// space as possible, then be placed next to eachother. Spacing between fragments can be
/// configured using `.spacing()`.
pub fn pack_tb<I: IntoIterator<Item=Frag>>(it: I) -> Frag {
  FragType::PackV { frags: it.into_iter().collect(), bottom_top: false, spacing: 0 }.into()
}

/// Like `pack_tb`, but fragment size are calculated from bottom to top.
pub fn pack_bt<I: IntoIterator<Item=Frag>>(it: I) -> Frag {
  FragType::PackV { frags: it.into_iter().collect(), bottom_top: true, spacing: 0 }.into()
}

/// Returns a fragment composed of the fragments produced by the given iterator. Each fragment
/// will take up as much space as possible, and will be drawn starting from the first fragment to
/// the last fragment. The total area taken up by this fragment will be the area taken up by the
/// largest sub-fragment.
pub fn overlay<I: IntoIterator<Item=Frag>>(it: I) -> Frag {
  FragType::Overlay { frags: it.into_iter().collect() }.into()
}

/// Creates a "container" fragment composed of just the given fragment, with the given edge padding.
pub fn container(frg: Frag, padding: Off) -> Frag {
  frg.container(padding)
}

impl Frag {

  /// Creates a "container" fragment composed of just this fragment, with the given edge padding.
  pub fn container(self, padding: Off) -> Frag {
    FragContainer::new(self, padding).into()
  }

  /// Equivalent to: overlay(vec![self, other])
  pub fn overlay(self, other: Frag) -> Frag {
    overlay(std::iter::once(self).chain(std::iter::once(other)))
  }

  /// Equivalent to: overlay(vec![other, self])
  pub fn underlay(self, other: Frag) -> Frag {
    overlay(std::iter::once(other).chain(std::iter::once(self)))
  }

  /// Creates a container fragment from this fragment, where the background of the container is
  /// filled with solid color `bg`.
  pub fn container_bg(self, padding: Off, bg: Color) -> Frag {
    if padding == 0 {
      fill(bg).overlay(self).container(0)
    } else {
      fill(bg).overlay(self.container(padding)).container(0)
    }
  }

  /// All fragments have an "alignment", which determines where they will be placed in a large region.
  /// The default alignment is `AlignH::Left`, `AlignV::Middle`.
  ///
  ///    let mut scene = frag::rect(Dim(4, 4), Color::RGB(255, 0, 0));
  ///    assert!(scene.calculate_geometry(Dim(10, 10)).pos(), Pos(0, 3));
  ///
  /// Alignment can be configured using the `align` function.
  ///
  ///    let mut scene = frag::rect(Dim(4, 4), Color::RGB(255, 0, 0))
  ///                      .align(AlignH::Center, AlignV::Bottom);
  ///    assert!(scene.calculate_geometry(Dim(10, 10)).pos(), Pos(3, 6));
  ///
  pub fn align(self, align_h: AlignH, align_v: AlignV) -> Frag {
    Frag { align_h, align_v, ..self }
  }

  /// Configures horizontal alignment.
  pub fn align_h(self, align_h: AlignH) -> Frag { Frag { align_h, ..self } }

  /// Configures vertical alignment.
  pub fn align_v(self, align_v: AlignV) -> Frag { Frag { align_v, ..self } }

  /// Configures the spacing between fragments for horizontal/vertical-packed fragments
  /// (`.pack_xy()`). For other fragments, this does nothing.
  pub fn spacing(mut self, pad: Off) -> Frag {
    match self.typ {
      FragType::PackH { ref mut spacing, .. } |
      FragType::PackV { ref mut spacing, .. } =>
        *spacing = pad,

      _ => ()
    }
    self
  }

  /// Configures the size of container or rectangle fragments. For other fragments, this does
  /// nothing. In order to impose size restrictions on other kinds of fragments, first place
  /// them in a container with `.container()`.
  pub fn dim(self, dim: Dim) -> Frag { self.on_dim(|_| dim) }
  pub fn width(self, w: Len) -> Frag { self.on_dim(|Dim(_, h)| Dim(w, h)) }
  pub fn height(self, h: Len) -> Frag { self.on_dim(|Dim(w, _)| Dim(w, h)) }

  fn on_dim<F>(mut self, f: F) -> Frag
  where F: FnOnce(Dim) -> Dim {
    match self.typ {
      FragType::Container(ref mut c) =>
        c.dim = f(c.dim),

      _ => ()
    }
    self
  }

  // Utils

  /// Returns an iterator producing the sub-fragments of this fragment.
  pub fn subfrags(&self) -> impl Iterator<Item=&Frag> {
    self.typ.subfrags()
  }
}

impl FragType {

  // Utils

  fn subfrags(&self) -> impl Iterator<Item=&Frag> {
    (0..self.subfrag_count()).map(move |i| self.subfrag(i))
  }

  fn subfrag_count(&self) -> usize {
    match *self {
      FragType::PackH { ref frags, .. } |
      FragType::PackV { ref frags, .. } |
      FragType::Overlay { ref frags } => frags.len(),
      FragType::Container { .. } => 1,
      _ => 0,
    }
  }

  fn subfrag(&self, i: usize) -> &Frag {
    match *self {
      FragType::PackH { ref frags, .. } |
      FragType::PackV { ref frags, .. } |
      FragType::Overlay { ref frags } => &frags[i],
      FragType::Container(ref fc) => &fc.inner,
      _ => unreachable!("subfrag(i) out of range")
    }
  }
}

//////////////////////////////////////////////////////////////////////////////////////////
// Calculating geometry

impl Frag {

  /// Recalculate's the absolute position and dimension of this fragment (and all sub-fragments) if
  /// contained in a region of the specified size. Returns the axis-aligned bounding-box encompassing
  /// this fragment.
  pub fn calculate_geometry(&mut self, dim: Dim) -> AABB {
    self.measure(dim);
    self.position(dim);
    AABB::new(self.calculated_offset, self.calculated_dim)
  }

  fn measure(&mut self, max_dim: Dim) {
    self.calculated_offset = Pos(0, 0);
    self.calculated_dim = self.typ.measure(max_dim);
  }

  fn position(&mut self, outer_dim: Dim) {
    self.calculated_offset.0 = match self.align_h {
      AlignH::Left => 0,
      AlignH::Right => (outer_dim.0 - self.calculated_dim.0) as Off,
      AlignH::Center => ((outer_dim.0 - self.calculated_dim.0) / 2) as Off,
    };
    self.calculated_offset.1 = match self.align_v {
      AlignV::Top => 0,
      AlignV::Bottom => (outer_dim.1 - self.calculated_dim.1) as Off,
      AlignV::Middle => ((outer_dim.1 - self.calculated_dim.1) / 2) as Off,
    };
  }
}

impl FragType {
  fn measure(&mut self, max_dim: Dim) -> Dim {
    match *self {
      FragType::Empty =>
        geom::EMPTY,

      FragType::Fill(_) =>
        max_dim,

      FragType::PackH { ref mut frags, right_left, spacing } => {
        let Dim(max_w, max_h) = max_dim;
        let (w, h) = pack(
          &mut *frags, spacing, max_w, right_left,
          |frg| frg.calculated_dim.0,
          |frg| frg.calculated_dim.1,
          |frg, rem_w| frg.measure(Dim(rem_w, max_h)),
          |frg, x, w, h| { frg.position(Dim(w, h)); frg.calculated_offset.0 += x; }
        );
        Dim(w, h)
      }

      FragType::PackV { ref mut frags, bottom_top, spacing } => {
        let Dim(max_w, max_h) = max_dim;
        let (h, w) = pack(
          &mut *frags, spacing, max_h, bottom_top,
          |frg| frg.calculated_dim.1,
          |frg| frg.calculated_dim.0,
          |frg, rem_h| frg.measure(Dim(max_w, rem_h)),
          |frg, y, h, w| { frg.position(Dim(w, h)); frg.calculated_offset.1 += y; }
        );
        Dim(w, h)
      }

      FragType::Overlay { ref mut frags } => {
        let mut min_dim = Dim(0, 0);
        for frg in frags.iter_mut() {
          frg.measure(max_dim);
          min_dim = Dim::max(min_dim, frg.calculated_dim);
        }
        for frg in frags.iter_mut() {
          frg.position(min_dim);
        }
        min_dim
      }

      FragType::Container(ref mut c) => {
        let &FragContainer { padding, dim, .. } = c.as_ref();
        let pad2 = (padding * 2) as Len;
        let outer_dim = Dim::min(dim, max_dim);
        let inner_dim = Dim(std::cmp::max(outer_dim.0, pad2) - pad2,
                            std::cmp::max(outer_dim.1, pad2) - pad2);
        c.inner.calculate_geometry(inner_dim);
        c.inner.calculated_offset.0 += padding;
        c.inner.calculated_offset.1 += padding;
        outer_dim
      }
    }
  }
}

// General "packing" algorithm, shared between PackH and PackV.
fn pack<X, F0, F1, FM, FP>(xs: &mut [X],
                           spacing: Off, space: Len, backwards: bool,
                           get_len0: F0, get_len1: F1,
                           measure: FM, position: FP) -> (Len, Len)
where F0: Fn(&X) -> Len,
      F1: Fn(&X) -> Len,
      FM: Fn(&mut X, Len) -> (),
      FP: Fn(&mut X, Off, Len, Len) -> (),
{
  let total_spacing = (spacing as Len) * (xs.len().saturating_sub(1) as Len);
  let mut sum0 = 0;
  let mut max1 = 0;

  // TODO: find a better way than this
  if backwards { xs.reverse() }

  for x in xs.iter_mut() {
    let remain0 = (space - sum0).saturating_sub(total_spacing);
    measure(x, remain0);
    sum0 += get_len0(x);
    max1 = std::cmp::max(max1, get_len1(x));
  }

  if backwards { xs.reverse() }

  let mut offset0 = 0;
  for x in xs.iter_mut() {
    let len0 = get_len0(x);
    position(x, offset0, len0, max1);
    offset0 += len0 as Off + spacing;
  }

  (sum0 + total_spacing, max1)
}

//////////////////////////////////////////////////////////////////////////////////////////
// Rendering

/// The `DrawFrag` trait represents a canvas that is able to draw arbitrary fragments.
/// Implementors must provide several low-level operations for drawing basic shapes. The
/// auto-derived method `draw_frag` displays the given fragment.
pub trait DrawFrag {

  /// Draw the given fragment onto this canvas.
  fn draw_frag(&mut self, frg: &Frag) {
    self.draw_frag_offset(frg, geom::ORIGIN)
  }

  /// Draw the fragment `frg`, offset by `offset` pixels from the top-left corner.
  fn draw_frag_offset(&mut self, frg: &Frag, mut offset: Pos) {
    offset.0 += frg.calculated_offset.0;
    offset.1 += frg.calculated_offset.1;
    render_frag_type(self, &frg.typ, AABB::new(offset, frg.calculated_dim))
  }

  /// Draw a axis-align bounding-box fill the specified color.
  fn fill_aabb(&mut self, aabb: AABB, color: Color);
}

fn render_frag_type<C: DrawFrag + ?Sized>(canv: &mut C, typ: &FragType, aabb: AABB) {
  match *typ {
    FragType::Empty => (),
    FragType::Fill(color) => canv.fill_aabb(aabb, color),
    _ =>
      for frg in typ.subfrags() {
        canv.draw_frag_offset(frg, aabb.pos())
      },
  }
}

impl<T> DrawFrag for sdl2::render::Canvas<T>
where T: sdl2::render::RenderTarget {
  fn fill_aabb(&mut self, aabb: AABB, color: Color) {
    self.set_draw_color(color);
    self.fill_rect(Some(aabb.into())).unwrap();
  }
}
